{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "14007fc2-be2a-497a-940d-4330bf2b46d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Imports\n",
    "\n",
    "from pgmpy.models import DynamicBayesianNetwork as DBN\n",
    "from pgmpy.factors.discrete import TabularCPD"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "9d5ea2e4-a280-4622-9045-13bc2b4cce2a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "06cfdf96f46c42c2a833c5021bd71a43",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/40 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: 1.1102230246251565e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n",
      "WARNING:pgmpy:Probability values don't exactly sum to 1. Differ by: -2.220446049250313e-16. Adjusting values.\n"
     ]
    }
   ],
   "source": [
    "# Initialize a simple DBN model modeling the Weather (W), Rain (O), Temperature (T), and Humidity (H).\n",
    "\n",
    "dbn = DBN()\n",
    "\n",
    "# pgmpy requires the user to define the structure of the first time slice and the edges connecting the first time slice to the second time slice.\n",
    "# pgmpy assumes that this structure remains constant for further time slices, i.e., it is a 2-TBN.\n",
    "\n",
    "# Add intra-slice edges for both time slices\n",
    "dbn.add_edges_from([\n",
    "    (('W', 0), ('O', 0)),  # Weather influences ground observation\n",
    "    (('T', 0), ('H', 0)),  # Temperature influences humidity\n",
    "    (('W', 0), ('H', 0))   # Weather influences humidity\n",
    "])\n",
    "\n",
    "# Add inter-slice edges\n",
    "dbn.add_edges_from([\n",
    "    (('W', 0), ('W', 1)),  # Weather transition\n",
    "    (('T', 0), ('T', 1)),  # Temperature transition\n",
    "    (('W', 0), ('T', 1))   # Weather influences future temperature\n",
    "])\n",
    "\n",
    "# Define the parameters of the model. Again pgmpy assumes that these CPDs remain the same for future time slices.\n",
    "\n",
    "# Define CPDs\n",
    "# CPD for W (Weather transition)\n",
    "cpd_w_0 = TabularCPD(\n",
    "    variable=('W', 0),\n",
    "    variable_card=3,  # Sunny, Cloudy, Rainy\n",
    "    values=[[0.6], [0.3], [0.1]],  # Initial probabilities\n",
    ")\n",
    "\n",
    "cpd_w_1 = TabularCPD(\n",
    "    variable=('W', 1),\n",
    "    variable_card=3,\n",
    "    evidence=[('W', 0)],\n",
    "    evidence_card=[3],\n",
    "    values=[\n",
    "        [0.7, 0.3, 0.2],  # P(Sunny | W_0)\n",
    "        [0.2, 0.4, 0.3],  # P(Cloudy | W_0)\n",
    "        [0.1, 0.3, 0.5]   # P(Rainy | W_0)\n",
    "    ],\n",
    ")\n",
    "\n",
    "# CPD for T (Temperature transition)\n",
    "cpd_t_0 = TabularCPD(\n",
    "    variable=('T', 0),\n",
    "    variable_card=3,  # Hot, Mild, Cold\n",
    "    values=[[0.5], [0.4], [0.1]]  # Initial probabilities\n",
    ")\n",
    "\n",
    "cpd_t_1 = TabularCPD(\n",
    "    variable=('T', 1),\n",
    "    variable_card=3,\n",
    "    evidence=[('T', 0), ('W', 0)],\n",
    "    evidence_card=[3, 3],\n",
    "    values=[\n",
    "        [0.8, 0.6, 0.1, 0.7, 0.4, 0.2, 0.6, 0.3, 0.1],  # P(Hot | T_0, W_0)\n",
    "        [0.2, 0.3, 0.7, 0.2, 0.5, 0.3, 0.3, 0.4, 0.3],  # P(Mild | T_0, W_0)\n",
    "        [0.0, 0.1, 0.2, 0.1, 0.1, 0.5, 0.1, 0.3, 0.6]   # P(Cold | T_0, W_0)\n",
    "    ]\n",
    ")\n",
    "\n",
    "# CPD for O (Ground observation)\n",
    "cpd_o = TabularCPD(\n",
    "    variable=('O', 0),\n",
    "    variable_card=2,  # Dry, Wet\n",
    "    evidence=[('W', 0)],\n",
    "    evidence_card=[3],\n",
    "    values=[\n",
    "        [0.9, 0.6, 0.2],  # P(Dry | Sunny, Cloudy, Rainy)\n",
    "        [0.1, 0.4, 0.8]   # P(Wet | Sunny, Cloudy, Rainy)\n",
    "    ]\n",
    ")\n",
    "\n",
    "# CPD for H (Humidity observation)\n",
    "cpd_h = TabularCPD(\n",
    "    variable=('H', 0),\n",
    "    variable_card=3,  # Low, Medium, High\n",
    "    evidence=[('T', 0), ('W', 0)],\n",
    "    evidence_card=[3, 3],\n",
    "    values=[\n",
    "        [0.7, 0.4, 0.1, 0.5, 0.3, 0.2, 0.3, 0.2, 0.1],  # P(Low | T_0, W_0)\n",
    "        [0.2, 0.5, 0.3, 0.4, 0.5, 0.3, 0.4, 0.3, 0.2],  # P(Medium | T_0, W_0)\n",
    "        [0.1, 0.1, 0.6, 0.1, 0.2, 0.5, 0.3, 0.5, 0.7]   # P(High | T_0, W_0)\n",
    "    ]\n",
    ")\n",
    "\n",
    "# Add CPDs to the DBN\n",
    "dbn.add_cpds(cpd_w_0, cpd_w_1, cpd_t_0, cpd_t_1, cpd_o, cpd_h)\n",
    "\n",
    "# After defining the model, call the initialization method that generates the required data structures for further computation.\n",
    "dbn.initialize_initial_state()\n",
    "\n",
    "# Simulate some data from the defined model.\n",
    "samples = dbn.simulate(n_samples=1000, n_time_slices=10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "7bf63016-f566-49cf-bd43-87c571b5f1c6",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T_0': 'C', 'W_0': 'C', 'H_0': 'C', 'O_0': 'C', 'T_1': 'C', 'W_1': 'C', 'H_1': 'C', 'O_1': 'C'}\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[<TabularCPD representing P((W, 0):3) at 0x747250050110>, <TabularCPD representing P((O, 0):2 | (W, 0):3) at 0x74713dd92570>, <TabularCPD representing P((H, 0):3 | (T, 0):3, (W, 0):3) at 0x74713dd93230>, <TabularCPD representing P((W, 1):3 | (W, 0):3) at 0x74713dd92ea0>, <TabularCPD representing P((T, 1):3 | (T, 0):3, (W, 0):3) at 0x74713dd925a0>, <TabularCPD representing P((O, 1):2 | (W, 1):3) at 0x74713dd93590>, <TabularCPD representing P((H, 1):3 | (T, 1):3, (W, 1):3) at 0x74713dd917c0>, <TabularCPD representing P((T, 0):3) at 0x74713dd927e0>]\n"
     ]
    }
   ],
   "source": [
    "# Fitting model parameters to a defined network structure.\n",
    "\n",
    "# Define the network structure for which to learn the model parameters. Here, we have assumeed the same model\n",
    "# structure that we simulated the data from\n",
    "dbn = DBN()\n",
    "dbn.add_edges_from([\n",
    "    (('W', 0), ('O', 0)),  # Weather influences ground observation\n",
    "    (('T', 0), ('H', 0)),  # Temperature influences humidity\n",
    "    (('W', 0), ('H', 0))   # Weather influences humidity\n",
    "])\n",
    "dbn.add_edges_from([\n",
    "    (('W', 0), ('W', 1)),  # Weather transition\n",
    "    (('T', 0), ('T', 1)),  # Temperature transition\n",
    "    (('W', 0), ('T', 1))   # Weather influences future temperature\n",
    "])\n",
    "\n",
    "# Fit the model using simulated samples\n",
    "dbn.fit(samples)\n",
    "print(dbn.cpds)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "bfd51960-dda5-40ea-a0e6-499e490c699e",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "/tmp/ipykernel_1736345/1916813029.py:18: FutureWarning: The behavior of DataFrame concatenation with empty or all-NA entries is deprecated. In a future version, this will no longer exclude empty or all-NA columns when determining the result dtypes. To retain the old behavior, exclude the relevant entries before the concat operation.\n",
      "  df_long = pd.concat([df_long, samples_t])\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T0': 'C', 'W0': 'C', 'H0': 'C', 'O0': 'C', 'T1': 'C', 'W1': 'C', 'H1': 'C', 'O1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T0': 'C', 'W0': 'C', 'H0': 'C', 'O0': 'C', 'T1': 'C', 'W1': 'C', 'H1': 'C', 'O1': 'C'}\n",
      "INFO:pgmpy: Datatype (N=numerical, C=Categorical Unordered, O=Categorical Ordered) inferred from data: \n",
      " {'T0': 'C', 'W0': 'C', 'H0': 'C', 'O0': 'C', 'T1': 'C', 'W1': 'C', 'H1': 'C', 'O1': 'C'}\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "5951e44f9bc644fabf1c69e75b069379",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/1000000 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[('T0', 'T1'), ('W0', 'T1'), ('W0', 'H0'), ('W0', 'W1'), ('W0', 'T0'), ('H0', 'T0'), ('O0', 'W0'), ('T1', 'H1'), ('W1', 'H1'), ('W1', 'O1')]\n"
     ]
    }
   ],
   "source": [
    "# Learning the model structure from data.\n",
    "\n",
    "# pgmpy doesn't implement any specific methods for DBN structure learning. This is a hackish method to utilize the \n",
    "# existing BN learning algorithms to estimate the structure of the DBN. Essentially, we remove the time-information from the\n",
    "# given data and try to learn the 2-DBN network that remains constant across time-slices.\n",
    "\n",
    "# First convert the given dataset into long form removing the time information such that it is suitable to learn the 2-DBN network.\n",
    "\n",
    "import pandas as pd\n",
    "\n",
    "colnames = [(node + '0') for node in dbn._nodes()] + [(node + '1') for node in dbn._nodes()]\n",
    "df_long = pd.DataFrame(columns=colnames)\n",
    "\n",
    "for t in range(9):\n",
    "    cols = [(node, t) for node in dbn._nodes()] + [(node, t+1) for node in dbn._nodes()]\n",
    "    samples_t = samples.loc[:, cols]\n",
    "    samples_t.columns = colnames\n",
    "    df_long = pd.concat([df_long, samples_t])\n",
    "\n",
    "df_long = df_long.reset_index(drop=True)\n",
    "\n",
    "\n",
    "# Use this long data frame to learn the first two time frames of the DBN. Because we are using structure learning algorithms we\n",
    "# need to add constraints such that the algorithm doesn't learn edges from time slice 1 to 0.\n",
    "from pgmpy.estimators import HillClimbSearch\n",
    "est = HillClimbSearch(df_long)\n",
    "dag = est.estimate(black_list=[('W1', 'W0'), ('W1', 'O0'), ('W1', 'T0'), ('W1', 'H0'),\n",
    "                               ('O1', 'W0'), ('O1', 'O0'), ('O1', 'T0'), ('O1', 'H0'), \n",
    "                               ('T1', 'W0'), ('T1', 'O0'), ('T1', 'T0'), ('T1', 'H0'), \n",
    "                               ('H1', 'W0'), ('H1', 'O0'), ('H1', 'T0'), ('H1', 'H0'),]) # Constraints to learn edges in only time forward direction.\n",
    "\n",
    "print(dag.edges()) # Use this learned DAG to define a DBN."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
